<!DOCTYPE HTML>
<html lang="ch" class="sidebar-visible no-js light">
    <head>
        <!-- Book generated using mdBook -->
        <meta charset="UTF-8">
        <title>实验2：异常处理和硬件中断 - nudt os lab book (2023)</title>


        <!-- Custom HTML head -->
        
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="theme-color" content="#ffffff" />

        <link rel="icon" href="favicon.svg">
        <link rel="shortcut icon" href="favicon.png">
        <link rel="stylesheet" href="css/variables.css">
        <link rel="stylesheet" href="css/general.css">
        <link rel="stylesheet" href="css/chrome.css">
        <link rel="stylesheet" href="css/print.css" media="print">

        <!-- Fonts -->
        <link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
        <link rel="stylesheet" href="fonts/fonts.css">

        <!-- Highlight.js Stylesheets -->
        <link rel="stylesheet" href="highlight.css">
        <link rel="stylesheet" href="tomorrow-night.css">
        <link rel="stylesheet" href="ayu-highlight.css">

        <!-- Custom theme stylesheets -->

    </head>
    <body>
    <div id="body-container">
        <!-- Provide site root to javascript -->
        <script>
            var path_to_root = "";
            var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
        </script>

        <!-- Work around some values being stored in localStorage wrapped in quotes -->
        <script>
            try {
                var theme = localStorage.getItem('mdbook-theme');
                var sidebar = localStorage.getItem('mdbook-sidebar');

                if (theme.startsWith('"') && theme.endsWith('"')) {
                    localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
                }

                if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
                    localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
                }
            } catch (e) { }
        </script>

        <!-- Set the theme before any content is loaded, prevents flash -->
        <script>
            var theme;
            try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
            if (theme === null || theme === undefined) { theme = default_theme; }
            var html = document.querySelector('html');
            html.classList.remove('no-js')
            html.classList.remove('light')
            html.classList.add(theme);
            html.classList.add('js');
        </script>

        <!-- Hide / unhide sidebar before it is displayed -->
        <script>
            var html = document.querySelector('html');
            var sidebar = null;
            if (document.body.clientWidth >= 1080) {
                try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
                sidebar = sidebar || 'visible';
            } else {
                sidebar = 'hidden';
            }
            html.classList.remove('sidebar-visible');
            html.classList.add("sidebar-" + sidebar);
        </script>

        <nav id="sidebar" class="sidebar" aria-label="Table of contents">
            <div class="sidebar-scrollbox">
                <ol class="chapter"><li class="chapter-item expanded affix "><li class="part-title">操作系统实验指南</li><li class="chapter-item expanded "><a href="lab_0.html"><strong aria-hidden="true">1.</strong> 准备：实验环境搭建</a></li><li class="chapter-item expanded "><a href="lab_1.html"><strong aria-hidden="true">2.</strong> 实验1：向Framebuffer中打印字符</a></li><li class="chapter-item expanded "><a href="lab_2.html" class="active"><strong aria-hidden="true">3.</strong> 实验2：异常处理和硬件中断</a></li><li class="chapter-item expanded "><a href="lab_3.html"><strong aria-hidden="true">4.</strong> 实验3：内存管理</a></li><li class="chapter-item expanded "><a href="lab_4.html"><strong aria-hidden="true">5.</strong> 实验4：协程与异步</a></li></ol>
            </div>
            <div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
        </nav>

        <div id="page-wrapper" class="page-wrapper">

            <div class="page">
                                <div id="menu-bar-hover-placeholder"></div>
                <div id="menu-bar" class="menu-bar sticky bordered">
                    <div class="left-buttons">
                        <button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
                            <i class="fa fa-bars"></i>
                        </button>
                        <button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
                            <i class="fa fa-paint-brush"></i>
                        </button>
                        <ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
                            <li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
                        </ul>
                        <button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
                            <i class="fa fa-search"></i>
                        </button>
                    </div>

                    <h1 class="menu-title">nudt os lab book (2023)</h1>

                    <div class="right-buttons">
                        <a href="print.html" title="Print this book" aria-label="Print this book">
                            <i id="print-button" class="fa fa-print"></i>
                        </a>

                    </div>
                </div>

                <div id="search-wrapper" class="hidden">
                    <form id="searchbar-outer" class="searchbar-outer">
                        <input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
                    </form>
                    <div id="searchresults-outer" class="searchresults-outer hidden">
                        <div id="searchresults-header" class="searchresults-header"></div>
                        <ul id="searchresults">
                        </ul>
                    </div>
                </div>

                <!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
                <script>
                    document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
                    document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
                    Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
                        link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
                    });
                </script>

                <div id="content" class="content">
                    <main>
                        <h1 id="实验2实现串口和鼠标中断"><a class="header" href="#实验2实现串口和鼠标中断">实验2：实现串口和鼠标中断</a></h1>
<h2 id="实验导读"><a class="header" href="#实验导读">实验导读</a></h2>
<p>大家已经完成了向Framebuffer输出字符的功能，相信已经对项目的整体结构、Rust的语法更加熟悉。本次实验需要对系统功能进行扩展，提供串口中断和鼠标中断处理(*鼠标中断为选做)，并且实现在QEMU终端和串口输入输出端各实现一个简单的命令解释器。</p>
<p>本实验提供一份基础代码，还有部分需要实验者自己补充，已在代码中用“TODO”标出。</p>
<ul>
<li><a href="#%E6%B7%BB%E5%8A%A0cpu%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E6%A8%A1%E5%9D%97">添加CPU异常处理模块</a>
<ul>
<li><a href="#%E4%B8%AD%E6%96%AD%E6%8F%8F%E8%BF%B0%E7%AC%A6%E8%A1%A8">中断描述符表</a></li>
<li><a href="#%E5%8C%85%E8%A3%85%E5%88%B0rust%E6%A8%A1%E5%9D%97">包装到Rust模块</a></li>
<li><a href="#%E6%B7%BB%E5%8A%A0%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B">添加测试用例</a></li>
</ul>
</li>
<li><a href="#%E6%B7%BB%E5%8A%A0%E6%97%B6%E9%92%9F%E5%92%8C%E9%94%AE%E7%9B%98%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86">添加时钟和键盘中断处理</a>
<ul>
<li><a href="#8259%E5%8F%AF%E7%BC%96%E7%A8%8B%E4%B8%AD%E6%96%AD%E6%8E%A7%E5%88%B6%E5%99%A8">8259可编程中断控制器</a></li>
<li><a href="#%E5%90%AF%E7%94%A8%E4%B8%AD%E6%96%AD">启用中断</a></li>
<li><a href="#%E8%AE%A1%E6%97%B6%E5%99%A8%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86">计时器中断处理</a></li>
<li><a href="#%E9%94%AE%E7%9B%98%E4%B8%AD%E6%96%AD%E5%A4%84%E7%90%86">键盘中断处理</a></li>
</ul>
</li>
<li><a href="#%E6%B7%BB%E5%8A%A0%E4%B8%B2%E5%8F%A3%E4%B8%AD%E6%96%AD%E5%92%8C%E9%BC%A0%E6%A0%87%E4%B8%AD%E6%96%AD%E5%8A%9F%E8%83%BD%E9%BC%A0%E6%A0%87%E4%B8%AD%E6%96%AD%E4%B8%BA%E9%80%89%E5%81%9A">添加串口中断和鼠标中断功能（*鼠标中断为选做）</a></li>
<li><a href="#%E5%91%BD%E4%BB%A4%E8%A7%A3%E9%87%8A%E5%99%A8%E5%88%9D%E6%AD%A5%E5%AE%9E%E7%8E%B0">命令解释器初步实现</a></li>
<li><a href="#%E5%B0%8F%E7%BB%93">小结</a></li>
<li><a href="#%E4%B8%8B%E4%B8%80%E4%B8%AA%E5%AE%9E%E9%AA%8C">下一个实验</a></li>
<li><a href="#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</a></li>
</ul>
<h3 id="添加cpu异常处理模块"><a class="header" href="#添加cpu异常处理模块">添加CPU异常处理模块</a></h3>
<p>CPU异常在很多情况下都有可能发生，比如访问无效的内存地址，或者在除法运算里除以0。当异常发生后，CPU会中断当前的工作，并立即根据异常类型调用对应的错误处理函数。</p>
<h4 id="中断描述符表"><a class="header" href="#中断描述符表">中断描述符表</a></h4>
<p>要捕捉CPU异常，我们需要设置一个 <em>中断描述符表</em> (<em>Interrupt Descriptor Table</em>, IDT)，用来捕获每一个异常。由于硬件层面会不加验证的直接使用，所以我们需要根据预定义格式直接写入数据。</p>
<p>我们直接使用<code>x86_64</code> crate 内置的 <a href="https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html"><code>InterruptDescriptorTable</code> 结构</a>对异常进行捕获。</p>
<h4 id="包装到rust模块"><a class="header" href="#包装到rust模块">包装到Rust模块</a></h4>
<p>首先在<code>kernel/src/driver/mod.rs</code>中创建中断描述符表模块：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/mod.rs中

// 中断描述符表
pub mod idt;
<span class="boring">}</span></code></pre></pre>
<p>创建<code>kernel/src/driver/idt.rs</code>，加入初始化函数，并使用<a href="https://www.felixcloutier.com/x86/lgdt:lidt">lidt</a>指令进行装载（<code>x86_64</code>的<code>InterruptDescriptorTable</code>结构提供了[load]函数）。<strong>注意</strong>，这里涉及到<strong>Rust语言生命周期</strong>、<strong>懒加载</strong>等概念，有兴趣的同学可以对参考资料1.进行扩展阅读。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中
use spin::Lazy;
use x86_64::structures::idt::InterruptDescriptorTable;

static IDT: Lazy&lt;InterruptDescriptorTable&gt; = Lazy::new(|| {
    let idt = InterruptDescriptorTable::new();
    idt
});

/// 初始化中断描述符表
pub fn init() {
    IDT.load();
}
<span class="boring">}</span></code></pre></pre>
<p>接下来添加处理函数，首先添加<code>breakpoint exception</code>作为一个测试途径。我们创建一个简单的<code>breakpoint_handler</code>方法并加入IDT中：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中
use spin::Lazy;
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};

static IDT: Lazy&lt;InterruptDescriptorTable&gt; = Lazy::new(|| {
    let mut idt = InterruptDescriptorTable::new();
    idt.breakpoint.set_handler_fn(breakpoint_handler);
    idt
});

/// 初始化中断描述符表
pub fn init() {
    IDT.load();
}

extern &quot;x86-interrupt&quot; fn breakpoint_handler(stack_frame: InterruptStackFrame) {
    fb_println!(&quot;EXCEPTION: BREAKPOINT\n{:#?}&quot;, stack_frame);
}
<span class="boring">}</span></code></pre></pre>
<p>同时，为了能够手动启用<code>x86-interrupt</code>特性，在<code>kernel/lib.rs</code>中加入<code>#![feature(abi_x86_interrupt)]</code>开关。</p>
<p>现在，可以在<code>kernel/lib.rs</code>中初始化中断描述符表：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/lib.rs中

/// 各类初始化函数
pub fn init&lt;'a&gt;(boot_info: &amp;'a mut bootloader_api::BootInfo) {
    ...

    // 初始化中断描述符表
    driver::idt::init();
}
<span class="boring">}</span></code></pre></pre>
<h4 id="添加测试用例"><a class="header" href="#添加测试用例">添加测试用例</a></h4>
<p><code>breakpoint exception</code>的唯一目的就是在<code>int3</code>指令执行时暂停程序运行，现在我们可以添加一个测试用例来确保以上工作可以顺利运行。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中

#[test_case]
fn test_breakpoint() {
    x86_64::instructions::interrupts::int3();
}
<span class="boring">}</span></code></pre></pre>
<p>在<code>kernel</code>文件夹下运行<code>cargo test --lib</code>命令后可看到：</p>
<p><img src="pic/lab2-breakpointtest-sc.png" alt="pic" /></p>
<blockquote>
<p><strong>练习 1</strong></p>
<ol>
<li>
<p>参考<a href="https://os.phil-opp.com/">blog_os</a>教程的<a href="https://os.phil-opp.com/cpu-exceptions/">CPU Exceptions</a>章节，并阅读<code>kernel/src/driver/idt.rs</code>中的代码，理解上述过程。</p>
</li>
<li>
<p>参考<a href="https://os.phil-opp.com/">blog_os</a>教程的<a href="https://os.phil-opp.com/double-fault-exceptions/">Double Fautls</a>章节，并阅读<code>kernel/src/driver/idt.rs</code>中的代码，梳理double fault异常处理流程。</p>
</li>
</ol>
</blockquote>
<h3 id="添加时钟和键盘中断处理"><a class="header" href="#添加时钟和键盘中断处理">添加时钟和键盘中断处理</a></h3>
<p>中断处理和异常处理很类似，只需要在中断描述符表中加入一个新条目即可。而CPU对中断的获取则通过统一的 <em>中断控制器</em> 代理。</p>
<h4 id="8259可编程中断控制器"><a class="header" href="#8259可编程中断控制器">8259可编程中断控制器</a></h4>
<p>我们直接使用<a href="https://docs.rs/pic8259/0.10.1/pic8259/">pic8259</a> crate对PIC进行配置和编程。首先将crate作为依赖加入工程中：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/Cargo.toml中

[dependencies]
pic8259 = &quot;0.10&quot;
<span class="boring">}</span></code></pre></pre>
<p>并在<code>kernel/src/driver/mod.rs</code>中声明模块：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/mod.rs中

// 中断控制器
pub mod pic;
<span class="boring">}</span></code></pre></pre>
<p>然后使用<a href="https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html">ChainedPics</a>映射PIC的布局：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

use pic8259::ChainedPics;
use spin::Mutex;

/// 一级PIC偏移
pub const PIC_1_OFFSET: u8 = 32;

/// 二级PIC偏移
pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;

/// 级联PIC驱动全局变量
pub static PICS: Mutex&lt;ChainedPics&gt; =
    Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });
<span class="boring">}</span></code></pre></pre>
<p>以上，我们成功将PIC的中断编号范围设定为了32–47。（PIC默认的配置其实是无法使用的，因为它仅仅是将0-15之间的中断向量编号发送给了CPU，然而这些编号已经用在了CPU的异常编号中了，比如8号代指 double fault 异常。要修复这个错误，我们需要对PIC中断序号进行重映射，新的序号只需要避开已被定义的CPU异常即可，CPU定义的异常数量有32个，所以通常会使用32-47这个区段）</p>
<p>同时，使用 <code>Mutex</code> 容器包裹了 <code>ChainedPics</code>，这样就可以通过（<a href="https://docs.rs/spin/0.5.2/spin/struct.Mutex.html#method.lock"><code>lock</code> 函数</a>）拿到被定义为安全的变量修改权限。</p>
<p>现在，我们就可以初始化 8259 PIC 配置了：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

/// 初始化中断控制器
pub fn init() {
    unsafe { PICS.lock().initialize() };
}
<span class="boring">}</span></code></pre></pre>
<p>并且在<code>kernel/lib.rs</code>中进行调用：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/lib.rs中

/// 各类初始化函数
pub fn init&lt;'a&gt;(boot_info: &amp;'a mut bootloader_api::BootInfo) {
    ...

    // 初始化中断控制器
    driver::pic::init();
}
<span class="boring">}</span></code></pre></pre>
<h4 id="启用中断"><a class="header" href="#启用中断">启用中断</a></h4>
<p>CPU现在还不能监听来自中断控制器的信息，因为CPU配置中中断处于禁用状态，因此，在初始化函数中增加如下方法进行启用：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

/// 初始化中断控制器
pub fn init() {
    unsafe { PICS.lock().initialize() };

    // CPU端开启中断
    x86_64::instructions::interrupts::enable();
}
<span class="boring">}</span></code></pre></pre>
<h4 id="计时器中断处理"><a class="header" href="#计时器中断处理">计时器中断处理</a></h4>
<p>我们需要增加对计时器中断信号的处理函数（否则运行后将抛出double fault异常）。首先，使用枚举类型<code>InterruptIndex</code>存储计时器的中断序号，为以后加入更多的中断枚举提供可扩展的编程风格。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
/// 外设中断编号
pub enum InterruptIndex {
    /// 时钟
    Timer = PIC_1_OFFSET,
}
<span class="boring">}</span></code></pre></pre>
<p>接下来在中断描述符表中为计时器中断添加一个处理函数<code>timer_interrupt_handler</code>，处理函数返回“中断结束”信号，该信号通知控制器中断已处理，系统可以接收下一个中断：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中
use crate::driver::pic::{notify_eoi, InterruptIndex};

static IDT: Lazy&lt;InterruptDescriptorTable&gt; = Lazy::new(|| {
    let mut idt = InterruptDescriptorTable::new();
    [...]
    idt[InterruptIndex::Timer as usize].set_handler_fn(timer_interrupt_handler);
    idt
});

extern &quot;x86-interrupt&quot; fn timer_interrupt_handler(_stack_frame: InterruptStackFrame) {
    notify_eoi(InterruptIndex::Timer as u8);
}
<span class="boring">}</span></code></pre></pre>
<p>中断结束信号的处理函数在<code>kernel/src/driver/pic.rs</code>中：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

/// 通知完成中断
pub fn notify_eoi(irq: u8) {
    unsafe {
        PICS.lock().notify_end_of_interrupt(irq);
    }
}
<span class="boring">}</span></code></pre></pre>
<h4 id="键盘中断处理"><a class="header" href="#键盘中断处理">键盘中断处理</a></h4>
<p>键盘中断处理的添加流程与计时器中断流程类似，首先是增加中断序号：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
/// 外设中断编号
pub enum InterruptIndex {
    /// 时钟
    Timer = PIC_1_OFFSET,
    /// 键盘
    Keyboard,
}
<span class="boring">}</span></code></pre></pre>
<p>然后是增加中断处理函数<code>keyboard_interrupt_handler</code>：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中
use crate::driver::pic::{notify_eoi, InterruptIndex};
use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1};

static IDT: Lazy&lt;InterruptDescriptorTable&gt; = Lazy::new(|| {
    let mut idt = InterruptDescriptorTable::new();
    [...]
    idt[InterruptIndex::Keyboard as usize].set_handler_fn(keyboard_interrupt_handler);
    idt
});

extern &quot;x86-interrupt&quot; fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {
    use x86_64::instructions::port::Port;
    let mut port = Port::new(0x60);
    let scancode: u8 = unsafe { port.read() };
    let mut keyboard = Keyboard::new(
        ScancodeSet1::new(),
        layouts::Us104Key,
        HandleControl::Ignore,
    );

    if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
        if let Some(key) = keyboard.process_keyevent(key_event) {
            match key {
                DecodedKey::Unicode(character) =&gt; {
                    fb_print!(&quot;{}&quot;, character);
                }
                DecodedKey::RawKey(_key) =&gt; {
                    fb_print!(&quot;{:?}&quot;, key);
                }
            }
        }
    }
    notify_eoi(InterruptIndex::Keyboard as u8);
}
<span class="boring">}</span></code></pre></pre>
<p>直接<code>make run</code>会发现键盘中断没有响应，因为教程中默认使用BIOS模式启动，PIC默认打开键盘中断；我们的内核默认使用UEFI模式启动，PIC默认关闭了键盘中断，可使用<code>make run boot=bios</code>命令运行，测试键盘中断。</p>
<p>这里对<strong>键盘中断</strong>、<strong>扫描码读取</strong>、<strong>扫描码转义</strong>的过程说明请参考<a href="https://os.phil-opp.com/">blog_os</a>教程的<a href="https://os.phil-opp.com/hardware-interrupts/">Hardware Interrupts</a>章节。</p>
<blockquote>
<p><strong>练习 2</strong></p>
<p>示例代码中的键盘中断实现未支持删除键的显示，请思考如何捕捉这一输入并在屏幕上实现删除字符的效果。</p>
</blockquote>
<h3 id="添加串口中断和鼠标中断功能鼠标中断为选做"><a class="header" href="#添加串口中断和鼠标中断功能鼠标中断为选做">添加串口中断和鼠标中断功能（*鼠标中断为选做）</a></h3>
<p>添加串口和鼠标中断的过程与计时器、键盘中断过程类似，首先需要增加中断序号：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/pic.rs中

#[derive(Debug, Clone, Copy)]
#[repr(u8)]
/// 外设中断编号
pub enum InterruptIndex {
    /// 时钟
    Timer = PIC_1_OFFSET,
    /// 键盘
    Keyboard,
    // TODO
}
<span class="boring">}</span></code></pre></pre>
<p>然后是增加中断处理函数：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// 在kernel/src/driver/idt.rs中
use crate::driver::pic::{notify_eoi, InterruptIndex};

static IDT: Lazy&lt;InterruptDescriptorTable&gt; = Lazy::new(|| {
    let mut idt = InterruptDescriptorTable::new();
    [...]
    // TODO
    idt
});

// TODO: 分别为串口和鼠标中断实现处理函数
extern &quot;x86-interrupt&quot; fn [...](_stack_frame: InterruptStackFrame) {
    
}
<span class="boring">}</span></code></pre></pre>
<p>串口中断的实现可以使用示例代码<code>kernel/src/driver/serial.rs</code>中的相关函数帮助实现，实现效果为可以在终端进行键盘输入并看到相应字符，如下图红色方框中字符所示：
<img src="pic/lab2-serial-sc.png" alt="pic" /></p>
<p>鼠标设备驱动的原理可以从<a href="https://wiki.osdev.org/PS/2_Mouse">资料</a>中获取，我们建议使用社区中已有的第三方库<a href="https://crates.io/crates/ps2-mouse">ps2-mouse</a>帮助实现，通过阅读参考库的示例代码和文档，方便地实现鼠标设备的支持和相关中断处理，实现效果为可以展示系统对鼠标事件（移动、点击等）的感知（最好能做出图形界面的效果，可以看到鼠标的移动轨迹，点击不同按键可以有不同效果等），参考实现效果如下图：
<img src="pic/lab2-finish-sc.png" alt="pic" /></p>
<blockquote>
<p><strong>练习 3</strong>
仿照已有的时钟中断和键盘中断处理流程，增加串口中断和鼠标中断（*鼠标中断为选做）。</p>
<p>提示</p>
<ul>
<li>一个中断一般涉及三个参与者：CPU、中断控制器（PIC）、设备，三者均需打开中断:
<ul>
<li>CPU中断总开关在<code>kernel/src/driver/pic.rs</code>文件的<code>init</code>函数中打开</li>
<li>PIC的中断开启方法参考我们用的<a href="https://crates.io/crates/pic8259">pic8259</a>库的<a href="https://docs.rs/pic8259/0.10.4/pic8259/struct.ChainedPics.html">手册</a></li>
<li>设备方面，串口设备已在初始化阶段打开中断，鼠标设备目前我们还没有相关驱动</li>
</ul>
</li>
</ul>
</blockquote>
<h3 id="命令解释器初步实现"><a class="header" href="#命令解释器初步实现">命令解释器初步实现</a></h3>
<p>大家在实验的过程中使用了命令解释器和电脑进行交互，包括查看文件、编译项目等等。目前我们的实验操作系统也已经拥有了显示功能、键盘输入功能并且支持串口中断，那么大家可以开始在这个实验操作系统上实现一个简单的命令解释器，并进行一些交互：一方面可以在QEMU上进行命令的解释交互，另一方面可以在串口终端上进行调试信息的输出。本次实验我们只会实现一个非常简单的概念命令解释器，随着后续操作系统功能的增加，再对这个命令解释器的功能进行逐步完善。</p>
<p>假设我们已经实现了一个shell模块（<code>crate::shell::shell_input</code>），那么在键盘输入的时候，就可以对输入的字符进行解析，用这个功能模块替换之前在显示器上单纯的输出：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>extern &quot;x86-interrupt&quot; fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {
    [...]

    if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
        if let Some(key) = keyboard.process_keyevent(key_event) {
            match key {
                DecodedKey::Unicode(character) =&gt; {
                    // 替换了之前fb_print!
                    // TODO: 请创建一个shell模块并实现shell_input功能
                    crate::shell::shell_input(character);
                }
                DecodedKey::RawKey(_key) =&gt; {
                    fb_print!(&quot;{:?}&quot;, key);
                }
            }
        }
    }
    [...]
}
<span class="boring">}</span></code></pre></pre>
<p>接下来，我们需要在<code>kernel</code>的根目录下新建一个<code>shell.rs</code>文件，实现<code>shell_input</code>功能。在实现过程中，如果大家引入了alloc库并且使用了String类型，会得到如下报错信息：</p>
<p><img src="pic/lab2-error.png" alt="pic" /></p>
<p>这是由于目前在我们的操作系统没有使用Rust的标准库（<code>#![no_std]</code>），而我们也还没有实现全局的内存分配器，因此无法使用这一动态数据类型。在实验3中，我们会增加内存管理的相关功能，并且将用新的方式实现一个更灵活的命令解释器。</p>
<p>在此之前，大家可以使用rust中的数组代替字符类型进行字符串的记录和匹配。可以声明一个全局的数组记录键盘的输入。</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>/// 处理键盘输入
pub fn shell_input(chr: char) {
    
    // TODO: 增加字符解析功能，实现以下功能
    /*
    1. 输入help+回车，显示帮助信息
    2. 输入clear+回车，清空屏幕显示
    3. 输入exit+回车，退出qemu终端
    4. 输入reboot+回车，重启终端
    5. 输入其他未定义命令+回车，弹出错误提示信息
    6. 其他功能：每行命令输出前有提示前缀；可以回退删除打错的字符
    */

}
<span class="boring">}</span></code></pre></pre>
<blockquote>
<p><strong>提示</strong></p>
<ul>
<li>退出：在kernel/src/test.rs中我们定义了exit_qemu函数</li>
<li>重启：我们使用的x86_64库没有封装重启系统函数，我们可以手写以下指令实现：</li>
</ul>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>core::arch::asm!(
    &quot;cli
    mov al, 0xfe
    out 0x64, al&quot;
);
<span class="boring">}</span></code></pre></pre>
</blockquote>
<p>完成效果如下图所示:</p>
<p><img src="pic/lab2-shell-final.png" alt="pic" /></p>
<p>除此之外，由于实现了串口中断和输入输出功能，我们同样可以在串口重定向的终端输入简单的调试命令获取相应的信息。实现方法和<code>shell_input</code>类似，大家可以在串口中断时对输入的字符串进行解析：</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>extern &quot;x86-interrupt&quot; fn com1_interrupt_handler(_stack_frame: InterruptStackFrame) {
    [...]
    // 替换了之前的serial_print！
    // TODO: 请在shell模块并实现serial_shell_input功能
    crate::shell::serial_shell_input(chr);
    [...]
}
<span class="boring">}</span></code></pre></pre>
<p>并在<code>shell.rs</code>文件，实现<code>serial_shell_input</code>函数，完成以下基本调试功能:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>/// 解析串口输入
pub fn serial_shell_input(chr: char) {
    
    // TODO: 增加字符解析功能，实现以下功能
    /*
    1. 输入time+回车，显示系统目前时钟中断次数
    2. 输入其他未定义命令+回车，弹出错误提示信息
    3. 其他功能：每行命令输出前有提示前缀；可以回退删除打错的字符
    */

}
<span class="boring">}</span></code></pre></pre>
<p>完成效果如下图所示:</p>
<p><img src="pic/lab2-serial-shell-sc.png" alt="pic" /></p>
<blockquote>
<p><strong>练习 4</strong></p>
<p>在QEMU终端和串口输入端各增加一个简单的命令解释器，完成代码TODO部分提到的基本功能。</p>
</blockquote>
<h2 id="小结"><a class="header" href="#小结">小结</a></h2>
<p>请在完成实验之后提交实验报告和所有源码，实验报告应记录实验过程和主要画面。</p>
<p>不同于Linux开发，在基于Rust的内核开发中，我们可以使用大量第三方库，学会库的使用会极大提升我们的开发效率。</p>
<p>通过本实验，相信大家能够对CPU异常处理过程和硬件中断过程有更加深入的理解。</p>
<h2 id="下一个实验"><a class="header" href="#下一个实验">下一个实验</a></h2>
<p>实现物理页帧分配器及物理页帧到虚拟页的映射。请提前复习内存管理相关知识点。</p>
<h2 id="参考资料"><a class="header" href="#参考资料">参考资料</a></h2>
<ol>
<li>blog_os中CPU异常章节：<a href="https://os.phil-opp.com/cpu-exceptions/">https://os.phil-opp.com/cpu-exceptions/</a></li>
<li>blog_os中硬件中断章节：<a href="https://os.phil-opp.com/hardware-interrupts/">https://os.phil-opp.com/hardware-interrupts/</a></li>
<li>PIC8259相关链接：
第三方库:<a href="https://crates.io/crates/pic8259">https://crates.io/crates/pic8259</a>
手册:<a href="https://docs.rs/pic8259/0.10.4/pic8259/struct.ChainedPics.html">https://docs.rs/pic8259/0.10.4/pic8259/struct.ChainedPics.html</a></li>
</ol>

                    </main>

                    <nav class="nav-wrapper" aria-label="Page navigation">
                        <!-- Mobile navigation buttons -->
                            <a rel="prev" href="lab_1.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
                                <i class="fa fa-angle-left"></i>
                            </a>

                            <a rel="next" href="lab_3.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
                                <i class="fa fa-angle-right"></i>
                            </a>

                        <div style="clear: both"></div>
                    </nav>
                </div>
            </div>

            <nav class="nav-wide-wrapper" aria-label="Page navigation">
                    <a rel="prev" href="lab_1.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
                        <i class="fa fa-angle-left"></i>
                    </a>

                    <a rel="next" href="lab_3.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
                        <i class="fa fa-angle-right"></i>
                    </a>
            </nav>

        </div>

        <!-- Livereload script (if served using the cli tool) -->
        <script>
            const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
            const wsAddress = wsProtocol + "//" + location.host + "/" + "__livereload";
            const socket = new WebSocket(wsAddress);
            socket.onmessage = function (event) {
                if (event.data === "reload") {
                    socket.close();
                    location.reload();
                }
            };

            window.onbeforeunload = function() {
                socket.close();
            }
        </script>



        <script>
            window.playground_copyable = true;
        </script>


        <script src="elasticlunr.min.js"></script>
        <script src="mark.min.js"></script>
        <script src="searcher.js"></script>

        <script src="clipboard.min.js"></script>
        <script src="highlight.js"></script>
        <script src="book.js"></script>

        <!-- Custom JS scripts -->


    </div>
    </body>
</html>
